package com.sardine.base.mybatis.interceptor;

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.DefaultReflectorFactory;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.reflection.factory.DefaultObjectFactory;
import org.apache.ibatis.reflection.factory.ObjectFactory;
import org.apache.ibatis.reflection.wrapper.DefaultObjectWrapperFactory;
import org.apache.ibatis.reflection.wrapper.ObjectWrapperFactory;
import org.apache.ibatis.session.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

/**
 * ****************************************
 * Created by IntelliJ IDEA 14.1.12
 * Mybatis采用责任链模式，通过动态代理组织多个拦截器（插件），通过这些拦截器可以改变Mybatis的默认行为（诸如SQL重写之类的），
 * 由于插件会深入到Mybatis的核心，因此在编写自己的插件前最好了解下它的原理，以便写出安全高效的插件。
 * Mybatis支持对Executor、StatementHandler、PameterHandler和ResultSetHandler进行拦截，也就是说会对这4种对象进行代理。
 *
 * @author daif
 * @version V1.0
 * @company 北京京东世纪贸易有限公司
 * @description helloProfile
 * @date 2015/5/31 15:08
 * email:   daifeng@jd.com
 * Copyright © 2015 JD Systems Incorporated. All rights reserved.
 * *****************************************
 */
/*从签名里可以看出，要拦截的目标类型是StatementHandler（注意：type只能配置成接口类型），拦截的方法是名称为prepare参数为Connection等类型的方法。*/
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class,Integer.class })})
public class MysqlDDLDMLInterceptor implements Interceptor {
    private static final Logger logger = LoggerFactory.getLogger(MysqlDDLDMLInterceptor.class);

    private static final ObjectFactory DEFAULT_OBJECT_FACTORY = new DefaultObjectFactory();
    private static final ObjectWrapperFactory DEFAULT_OBJECT_WRAPPER_FACTORY = new DefaultObjectWrapperFactory();
    private static final ReflectorFactory DEFAULT_OBJECT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
    private static String defaultDialect = "mysql"; // 数据库类型(默认为mysql)
    private static String defaultPageSqlIdEndsWithPrefix = ".*D[D,M]L$"; // 需要拦截的ID(正则匹配)

    private String dialect = ""; // 数据库类型(默认为mysql)
    private String pageSqlIdEndsWithPrefix = ""; // 需要拦截的ID(正则匹配)

    /**
     * 是实现拦截逻辑的地方，内部要通过invocation.proceed()显式地推进责任链前进，也就是调用下一个拦截器拦截目标方法。
     *
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MetaObject metaStatementHandler = MetaObject.forObject(statementHandler, DEFAULT_OBJECT_FACTORY,
                DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_OBJECT_REFLECTOR_FACTORY);
        // 分离代理对象链(由于目标类可能被多个拦截器拦截，从而形成多次代理，通过下面的两次循环可以分离出最原始的的目标类)
        while (metaStatementHandler.hasGetter("h")) {
            Object object = metaStatementHandler.getValue("h");
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_OBJECT_REFLECTOR_FACTORY);
        }
        // 分离最后一个代理对象的目标类
        while (metaStatementHandler.hasGetter("target")) {
            Object object = metaStatementHandler.getValue("target");
            metaStatementHandler = MetaObject.forObject(object, DEFAULT_OBJECT_FACTORY, DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_OBJECT_REFLECTOR_FACTORY);
        }
        //配置拦截器全局的其他属性
        interceptorGlobalConfig(metaStatementHandler);
        MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
        // 只重写需要分页的sql语句。通过MappedStatement的ID匹配，默认重写以Page结尾的MappedStatement的sql
        BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
        if (mappedStatement.getId().matches(pageSqlIdEndsWithPrefix)) {
            Object parameterObject = boundSql.getParameterObject();
            if (parameterObject == null) {
                throw new NullPointerException("parameterObject is null!");
            } else {
                //获取被拦截的方法参数
                Connection connection = (Connection) invocation.getArgs()[0];
                // 重设分页参数里的总页数等
                executeDDLDML(connection, mappedStatement, boundSql);
            }
        }
        // 将执行权交给下一个拦截器
        return invocation.proceed();
    }

    private int executeDDLDML(Connection connection, MappedStatement mappedStatement, BoundSql boundSql) {
        Statement countStmt = null;
        try {
            countStmt = connection.createStatement();
            Object parameterObject = boundSql.getParameterObject();
            // 发送sql语句
            int result = countStmt.executeUpdate(parameterObject.toString());
            return result;
        } catch (SQLException e) {
            throw new RuntimeException("执行DDL DML 异常",e);
        } finally {
            try {
                if (countStmt != null) {
                    countStmt.close();
                }
            } catch (SQLException xs) {
                logger.error("Ignore this exception", xs);
            }
        }
    }

    /**
     * 就是用当前这个拦截器生成对目标target的代理，实际是通过Plugin.wrap(target,this) 来完成的，把目标target和拦截器this传给了包装函数。
     *
     * @param target
     * @return
     */
    @Override
    public Object plugin(Object target) {
        // 当目标类是StatementHandler类型时，才包装目标类，否者直接返回目标本身,减少目标被代理的次数
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this);
        } else {
            return target;
        }
    }

    /**
     * 用于设置额外的参数，参数配置在拦截器的Properties节点里。
     * <p/>
     * 注解里描述的是指定拦截方法的签名  [type,method,args] （即对哪种对象的哪种方法进行拦截），它在拦截前用于决断。
     *
     * @param properties
     */
    @Override
    public void setProperties(Properties properties) {
        //设置分页sql语句的id 结束符号
        this.pageSqlIdEndsWithPrefix = properties.getProperty("pageSqlIdEndsWithPrefix");
        //设置分页方言
        this.dialect = properties.getProperty("dialect");
    }

    /**
     * 从mybatis文件中读取全局属性，如果属性
     *
     * @param metaStatementHandler
     */
    private void interceptorGlobalConfig(MetaObject metaStatementHandler) {
        Configuration configuration = (Configuration) metaStatementHandler.getValue("delegate.configuration");
        //检查是否拦截器自己配置了属性
        if (null == dialect || "".equals(dialect)) {
            //没有配置就使用全局配置
            dialect = configuration.getVariables().getProperty("dialect");
        }
        //全局配置也没有，就使用默认配置
        if (null == dialect || "".equals(dialect)) {
            logger.warn("Property dialect is not setted,use default 'mysql' ");
            dialect = defaultDialect;
        }
        //分页查询ID结束标示
        if (null == pageSqlIdEndsWithPrefix || "".equals(pageSqlIdEndsWithPrefix)) {
            pageSqlIdEndsWithPrefix = configuration.getVariables().getProperty("pageSqlIdEndsWithPrefix");
        }
        if (null == pageSqlIdEndsWithPrefix || "".equals(pageSqlIdEndsWithPrefix)) {
            logger.warn("Property pageSqlIdEndsWithPrefix is not setted,use default '.*Page$' ");
            pageSqlIdEndsWithPrefix = defaultPageSqlIdEndsWithPrefix;
        }
    }

}
